/*
MIT LICENSE
Copyright (c) 2007 Monsur Hossain (http://monsur.hossa.in)

Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
*/

// Avoid polluting the global namespace if we're using a module loader
(function () {
	/**
	 * Creates a new Cache object.
	 * @param {number} maxSize The maximum size of the cache (or -1 for no max).
	 * @param {boolean} debug Whether to log events to the console.log.
	 * @constructor
	 */
	function Cache(maxSize, debug, storage) {
		this.maxSize_ = maxSize || -1;
		this.debug_ = debug || false;
		this.storage_ = storage || new Cache.BasicCacheStorage();

		this.fillFactor_ = 0.75;

		this.stats_ = {};
		this.stats_["hits"] = 0;
		this.stats_["misses"] = 0;
		this.log_("Initialized cache with size " + maxSize);
	}

	/**
	 * An easier way to refer to the priority of a cache item
	 * @enum {number}
	 */
	Cache.Priority = {
		LOW: 1,
		NORMAL: 2,
		HIGH: 4,
	};

	/**
	 * Basic in memory cache storage backend.
	 * @constructor
	 */
	Cache.BasicCacheStorage = function () {
		this.items_ = {};
		this.count_ = 0;
	};
	Cache.BasicCacheStorage.prototype.get = function (key) {
		return this.items_[key];
	};
	Cache.BasicCacheStorage.prototype.set = function (key, value) {
		if (typeof this.get(key) === "undefined") this.count_++;
		this.items_[key] = value;
	};
	Cache.BasicCacheStorage.prototype.size = function (key, value) {
		return this.count_;
	};
	Cache.BasicCacheStorage.prototype.remove = function (key) {
		var item = this.get(key);
		if (typeof item !== "undefined") this.count_--;
		delete this.items_[key];
		return item;
	};
	Cache.BasicCacheStorage.prototype.keys = function () {
		var ret = [],
			p;
		for (p in this.items_) ret.push(p);
		return ret;
	};

	/**
	 * Local Storage based persistant cache storage backend.
	 * If a size of -1 is used, it will purge itself when localStorage
	 * is filled. This is 5MB on Chrome/Safari.
	 * WARNING: The amortized cost of this cache is very low, however,
	 * when a the cache fills up all of localStorage, and a purge is required, it can
	 * take a few seconds to fetch all the keys and values in storage.
	 * Since localStorage doesn't have namespacing, this means that even if this
	 * individual cache is small, it can take this time if there are lots of other
	 * other keys in localStorage.
	 *
	 * @param {string} namespace A string to namespace the items in localStorage. Defaults to 'default'.
	 * @constructor
	 */
	Cache.LocalStorageCacheStorage = function (namespace) {
		this.prefix_ = "cache-storage." + (namespace || "default") + ".";
		// Regexp String Escaping from http://simonwillison.net/2006/Jan/20/escape/#p-6
		var escapedPrefix = this.prefix_.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
		this.regexp_ = new RegExp("^" + escapedPrefix);
	};
	Cache.LocalStorageCacheStorage.prototype.get = function (key) {
		var item = window.localStorage[this.prefix_ + key];
		if (item) return JSON.parse(item);
		return null;
	};
	Cache.LocalStorageCacheStorage.prototype.set = function (key, value) {
		window.localStorage[this.prefix_ + key] = JSON.stringify(value);
	};
	Cache.LocalStorageCacheStorage.prototype.size = function (key, value) {
		return this.keys().length;
	};
	Cache.LocalStorageCacheStorage.prototype.remove = function (key) {
		var item = this.get(key);
		delete window.localStorage[this.prefix_ + key];
		return item;
	};
	Cache.LocalStorageCacheStorage.prototype.keys = function () {
		var ret = [],
			p;
		for (p in window.localStorage) {
			if (p.match(this.regexp_)) ret.push(p.replace(this.prefix_, ""));
		}
		return ret;
	};

	/**
	 * Session Storage based persistant cache storage backend.
	 * If a size of -1 is used, it will purge itself when sessionStorage
	 * is filled. This is 5MB on Chrome/Safari.
	 * WARNING: The amortized cost of this cache is very low, however,
	 * when a the cache fills up all of sessionStorage, and a purge is required, it can
	 * take a few seconds to fetch all the keys and values in storage.
	 * Since sessionStorage doesn't have namespacing, this means that even if this
	 * individual cache is small, it can take this time if there are lots of other
	 * other keys in sessionStorage.
	 *
	 * @param {string} namespace A string to namespace the items in sessionStorage. Defaults to 'default'.
	 * @constructor
	 */
	Cache.SessionStorageCacheStorage = function (namespace) {
		this.prefix_ = "cache-storage." + (namespace || "default") + ".";
		// Regexp String Escaping from http://simonwillison.net/2006/Jan/20/escape/#p-6
		var escapedPrefix = this.prefix_.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
		this.regexp_ = new RegExp("^" + escapedPrefix);
	};
	Cache.SessionStorageCacheStorage.prototype.get = function (key) {
		var item = window.sessionStorage[this.prefix_ + key];
		if (item) return JSON.parse(item);
		return null;
	};
	Cache.SessionStorageCacheStorage.prototype.set = function (key, value) {
		window.sessionStorage[this.prefix_ + key] = JSON.stringify(value);
	};
	Cache.SessionStorageCacheStorage.prototype.size = function (key, value) {
		return this.keys().length;
	};
	Cache.SessionStorageCacheStorage.prototype.remove = function (key) {
		var item = this.get(key);
		delete window.sessionStorage[this.prefix_ + key];
		return item;
	};
	Cache.SessionStorageCacheStorage.prototype.keys = function () {
		var ret = [],
			p;
		for (p in window.sessionStorage) {
			if (p.match(this.regexp_)) ret.push(p.replace(this.prefix_, ""));
		}
		return ret;
	};

	/**
	 * Retrieves an item from the cache.
	 * @param {string} key The key to retrieve.
	 * @return {Object} The item, or null if it doesn't exist.
	 */
	Cache.prototype.getItem = function (key) {
		// retrieve the item from the cache
		var item = this.storage_.get(key);

		if (item != null) {
			if (!this.isExpired_(item)) {
				// if the item is not expired
				// update its last accessed date
				item.lastAccessed = new Date().getTime();
			} else {
				// if the item is expired, remove it from the cache
				this.removeItem(key);
				item = null;
			}
		}

		// return the item value (if it exists), or null
		var returnVal = item ? item.value : null;
		if (returnVal) {
			this.stats_["hits"]++;
			this.log_("Cache HIT for key " + key);
		} else {
			this.stats_["misses"]++;
			this.log_("Cache MISS for key " + key);
		}
		return returnVal;
	};

	Cache._CacheItem = function (k, v, o) {
		if (!k) {
			throw new Error("key cannot be null or empty");
		}
		this.key = k;
		this.value = v;
		o = o || {};
		if (o.expirationAbsolute) {
			o.expirationAbsolute = o.expirationAbsolute.getTime();
		}
		if (!o.priority) {
			o.priority = Cache.Priority.NORMAL;
		}
		this.options = o;
		this.lastAccessed = new Date().getTime();
	};

	/**
	 * Sets an item in the cache.
	 * @param {string} key The key to refer to the item.
	 * @param {Object} value The item to cache.
	 * @param {Object} options an optional object which controls various caching
	 *    options:
	 *      expirationAbsolute: the datetime when the item should expire
	 *      expirationSliding: an integer representing the seconds since
	 *                         the last cache access after which the item
	 *                         should expire
	 *      priority: How important it is to leave this item in the cache.
	 *                You can use the values Cache.Priority.LOW, .NORMAL, or
	 *                .HIGH, or you can just use an integer.  Note that
	 *                placing a priority on an item does not guarantee
	 *                it will remain in cache.  It can still be purged if
	 *                an expiration is hit, or if the cache is full.
	 *      callback: A function that gets called when the item is purged
	 *                from cache.  The key and value of the removed item
	 *                are passed as parameters to the callback function.
	 */
	Cache.prototype.setItem = function (key, value, options) {
		// add a new cache item to the cache
		if (this.storage_.get(key) != null) {
			this.removeItem(key);
		}
		this.addItem_(new Cache._CacheItem(key, value, options));
		this.log_("Setting key " + key);

		// if the cache is full, purge it
		if (this.maxSize_ > 0 && this.size() > this.maxSize_) {
			var that = this;
			setTimeout(function () {
				that.purge_.call(that);
			}, 0);
		}
	};

	/**
	 * Removes all items from the cache.
	 */
	Cache.prototype.clear = function () {
		// loop through each item in the cache and remove it
		var keys = this.storage_.keys();
		for (var i = 0; i < keys.length; i++) {
			this.removeItem(keys[i]);
		}
		this.log_("Cache cleared");
	};

	/**
	 * @return {Object} The hits and misses on the cache.
	 */
	Cache.prototype.getStats = function () {
		return this.stats_;
	};

	/**
	 * @return {string} Returns an HTML string representation of the cache.
	 */
	Cache.prototype.toHtmlString = function () {
		var returnStr = this.size() + " item(s) in cache<br /><ul>";
		var keys = this.storage_.keys();
		for (var i = 0; i < keys.length; i++) {
			var item = this.storage_.get(keys[i]);
			returnStr = returnStr + "<li>" + item.key.toString() + " = " + item.value.toString() + "</li>";
		}
		returnStr = returnStr + "</ul>";
		return returnStr;
	};

	/**
	 * Allows it to resize the Cache capacity if needed.
	 * @param	{integer} newMaxSize the new max amount of stored entries within the Cache
	 */
	Cache.prototype.resize = function (newMaxSize) {
		this.log_("Resizing Cache from " + this.maxSize_ + " to " + newMaxSize);
		// Set new size before purging so we know how many items to purge
		var oldMaxSize = this.maxSize_;
		this.maxSize_ = newMaxSize;

		if (newMaxSize > 0 && (oldMaxSize < 0 || newMaxSize < oldMaxSize)) {
			if (this.size() > newMaxSize) {
				// Cache needs to be purged as it does contain too much entries for the new size
				this.purge_();
			} // else if cache isn't filled up to the new limit nothing is to do
		}
		// else if newMaxSize >= maxSize nothing to do
		this.log_("Resizing done");
	};

	/**
	 * Removes expired items from the cache.
	 */
	Cache.prototype.purge_ = function () {
		var tmparray = new Array();
		var purgeSize = Math.round(this.maxSize_ * this.fillFactor_);
		if (this.maxSize_ < 0) purgeSize = this.size() * this.fillFactor_;
		// loop through the cache, expire items that should be expired
		// otherwise, add the item to an array
		var keys = this.storage_.keys();
		for (var i = 0; i < keys.length; i++) {
			var key = keys[i];
			var item = this.storage_.get(key);
			if (this.isExpired_(item)) {
				this.removeItem(key);
			} else {
				tmparray.push(item);
			}
		}

		if (tmparray.length > purgeSize) {
			// sort this array based on cache priority and the last accessed date
			tmparray = tmparray.sort(function (a, b) {
				if (a.options.priority != b.options.priority) {
					return b.options.priority - a.options.priority;
				} else {
					return b.lastAccessed - a.lastAccessed;
				}
			});
			// remove items from the end of the array
			while (tmparray.length > purgeSize) {
				var ritem = tmparray.pop();
				this.removeItem(ritem.key);
			}
		}
		this.log_("Purged cached");
	};

	/**
	 * Add an item to the cache.
	 * @param {Object} item The cache item to add.
	 * @private
	 */
	Cache.prototype.addItem_ = function (item, attemptedAlready) {
		var cache = this;
		try {
			this.storage_.set(item.key, item);
		} catch (err) {
			if (attemptedAlready) {
				this.log_("Failed setting again, giving up: " + err.toString());
				throw err;
			}
			this.log_("Error adding item, purging and trying again: " + err.toString());
			this.purge_();
			this.addItem_(item, true);
		}
	};

	/**
	 * Remove an item from the cache, call the callback function (if it exists).
	 * @param {String} key The key of the item to remove
	 */
	Cache.prototype.removeItem = function (key) {
		var item = this.storage_.remove(key);
		this.log_("removed key " + key);

		// if there is a callback function, call it at the end of execution
		if (item && item.options && item.options.callback) {
			setTimeout(function () {
				item.options.callback.call(null, item.key, item.value);
			}, 0);
		}
		return item ? item.value : null;
	};

	/**
	 * Scan through each item in the cache and remove that item if it passes the
	 * supplied test.
	 * @param {Function} test   A test to determine if the given item should be removed.
	 *							The item will be removed if test(key, value) returns true.
	 */
	Cache.prototype.removeWhere = function (test) {
		// Get a copy of the keys array - it won't be modified when we remove items from storage
		var keys = this.storage_.keys();
		for (var i = 0; i < keys.length; i++) {
			var key = keys[i];
			var item = this.storage_.get(key);
			if (test(key, item.value) === true) {
				this.removeItem(key);
			}
		}
	};

	Cache.prototype.size = function () {
		return this.storage_.size();
	};

	/**
	 * @param {Object} item A cache item.
	 * @return {boolean} True if the item is expired
	 * @private
	 */
	Cache.prototype.isExpired_ = function (item) {
		var now = new Date().getTime();
		var expired = false;
		if (item.options.expirationAbsolute && item.options.expirationAbsolute < now) {
			// if the absolute expiration has passed, expire the item
			expired = true;
		}
		if (!expired && item.options.expirationSliding) {
			// if the sliding expiration has passed, expire the item
			var lastAccess = item.lastAccessed + item.options.expirationSliding * 1000;
			if (lastAccess < now) {
				expired = true;
			}
		}
		return expired;
	};

	/**
	 * Logs a message to the console.log if debug is set to true.
	 * @param {string} msg The message to log.
	 * @private
	 */
	Cache.prototype.log_ = function (msg) {
		if (this.debug_) {
			console.log(msg);
		}
	};

	// Establish the root object, `window` in the browser, or `global` on the server.
	var root = this;

	if (typeof module !== "undefined" && module.exports) {
		module.exports = Cache;
	} else if (typeof define == "function" && define.amd) {
		define(function () {
			return Cache;
		});
	} else {
		root.Cache = Cache;
	}
})();
if (typeof PromiseQueuePlus !== "undefined" && typeof scheduler === "undefined") {
	function getConcurrency() {
		if (window.navigator && window.navigator.hardwareConcurrency) return window.navigator.hardwareConcurrency;
		return 4;
	}
	this.promise_queue = PromiseQueuePlus(getConcurrency(), {
		autoRun: true,
	});
	this.promise_queue.start();
	if (window !== this) window.promise_queue = this.promise_queue;
}
function hasSVGSupport() {
	return !!document.createElementNS && !!document.createElementNS("http://www.w3.org/2000/svg", "svg").createSVGRect;
}
const nTube = {
	version: "1.5.9",
	versionInt: 1.5,
	nTubeUtils: {
		Quirks: {
			FORCE_MEDIA_CONTROLLER: typeof InstallTrigger !== "undefined",
			EMULATED_MSE_VIA_MMS: false,
		},
		isToday: function (dateParameter) {
			if (!window.Date) {
				return false;
			}
			var today = new Date();
			return dateParameter.getDate() === today.getDate() && dateParameter.getMonth() === today.getMonth() && dateParameter.getFullYear() === today.getFullYear();
		},
		getRandomInt: function (min, max) {
			min = Math.ceil(min);
			max = Math.floor(max);
			return Math.floor(Math.random() * (max - min + 1)) + min;
		},
		subtitleFor: function (type) {
			if (type === null || type === undefined) {
				/// teletext needs 45px on padding-left
				return null;
			} else if (type === "new") {
				return "::cue, video::cue, video::-webkit-media-text-track-display {padding: 5px; border: none; font-family: inherit; font-size: 2.6vw; background-color: rgba(0, 0, 0, 0); text-shadow: -1.5px -1.5px 0 #000, 1.5px -1.5px 0 #000, -1.5px 1.5px 0 #000, 1.5px 1.5px 0 #000; line-break: loose;}";
			} else if (type === "new_shadowed") {
				return "::cue, video::cue, video::-webkit-media-text-track-display {padding: 5px; border: none; font-family: inherit; font-size: 2.6vw; background-color: rgba(0, 0, 0, 0); text-shadow: black 2px 2px; line-break: loose;}";
			} else if (type === "new_outlined") {
				return "::cue, video::cue, video::-webkit-media-text-track-display {padding: 5px; border: none; font-family: inherit; font-size: 2.6vw; background-color: rgba(0, 0, 0, 0); text-shadow: -1.1px -1.1px 0 #000, 1.1px -1.1px 0 #000, -1.1px 1.1px 0 #000, 1.1px 1.1px 0 #000; font-weight: bold; line-break: loose;}";
			} else if (type === "new_tv_cc") {
				return "::cue, video::cue, video::-webkit-media-text-track-display {padding: 5px; border: none; font-family: 'Retron2000', 'VCR OSD Mono', monospace, sans-serif; font-size: 2.6vw; -webkit-text-stroke: black 1px; line-break: loose;}";
			} else if (type === "copyish") {
				return "::cue, video::cue, video::-webkit-media-text-track-display {padding: 5px; border: none; font-family: 'Inter', 'Roboto Condensed', 'Roboto', 'Product Sans', sans-serif; font-size: 2.6vw; text-shadow: -1.5px -1.5px 0 #000, 1.5px -1.5px 0 #000, -1.5px 1.5px 0 #000, 1.5px 1.5px 0 #000; border-radius: 5px; line-break: loose;}";
			} else if (type === "teletext") {
				return "::cue, video::cue, video::-webkit-media-text-track-display {padding: 5px; font-family: 'Tiresias Infofont', 'European Teletext Neuvo', 'European Teletext', 'TIFAX', 'ModeSeven', 'Retron2000', 'VCR OSD Mono', fixed, monospace, sans-serif; -webkit-text-stroke: black 0.5px; line-break: loose; text-align: left !important; font-smooth: never; -webkit-font-smoothing: none; padding-left: 45px; top: 80% !important;}";
			} else if (type === "hcontrast") {
				return '::cue, video::cue, div[pseudo="cue"] {padding: 5px; border: none; font-family: inherit; font-size: 3vw; background-color: black; line-break: loose; font-weight: 600;}';
			} else if (type === "yellow_on_black") {
				return "::cue, video::cue {padding: 5px; border: none; font-family: 'Inter', 'SF Pro Display',  'Roboto Condensed', 'Roboto', 'Product Sans', sans-serif; background: rgba(0, 0, 0, 0.3); font-size: 2.6vw; text-shadow: -1.5px -1.5px 0 #000, 1.5px -1.5px 0 #000, -1.5px 1.5px 0 #000, 1.5px 1.5px 0 #000; color: yellow; border-radius: 5px; line-break: loose;}";
			} else {
				return null;
			}
		},
		isPlaybackEngineDASH: function (engine) {
			switch (engine) {
				/* Stable */
				case "rxplayer":
				case "dash.js":
					return true;
				/* Experiments */
				/* */
				default:
					return false;
			}
		},
		getErrorReason: function (object) {
			if (object === null || object === undefined) {
				return null;
			} else if (object["error"] && object["reason"]) {
				/* nTube modern error format */
				return object["reason"];
			} else if (object["error"] && typeof object["error"] === "string") {
				/* nTube legacy error format */
				return object["error"];
			} else if (object["error_id"] && object["error_message"]) {
				/* nTube legacy error format */
				return object["error_message"];
			} else {
				return null;
			}
		},
	},
	init: function () {
		if (!Math.average) {
			Math.average = function () {
				if (arguments.length == 0) {
					return 0;
				} else {
					if (arguments.length == 1 && Array.isArray(arguments[0])) {
						var n = 0;
						for (var i = 0; i < arguments[0].length; i++) {
							n += arguments[0][i];
						}
						return n / arguments[0].length;
					} else {
						var n = 0;
						for (var i = 0; i < arguments.length; i++) {
							n += arguments[i];
						}
						return n / arguments.length;
					}
				}
			};
		}
		libWeb.TaskDispatcher.run_promise(function () {
			const vlabel = id("ntube_version_main_menu_label");
			if (vlabel !== null) {
				vlabel.innerHTML = nTube.version;
			}
		});
		libWeb.TaskDispatcher.run_promise(function () {
			if (window.Date) {
				const date = new Date();
				const isNTBirthday = nTube.nTubeUtils.isToday(new Date(date.getFullYear(), 06, [14]));
				const isSpooky = nTube.nTubeUtils.isToday(new Date(date.getFullYear(), 9, [31]));
				const isChristmas = date >= new Date(date.getFullYear(), 11, [6]) && date <= new Date(date.getFullYear(), 11, [27]);
				if (isNTBirthday) {
					if (id("ntube-winter-logo") != null) {
						id("ntube-winter-logo").src = "resources/events/birthday_hat.png";
						id("ntube-winter-logo").title = "Happy birthday, nTube!";
						id("ntube-winter-logo").style.marginLeft = "6px";
						id("ntube-winter-logo").style.display = "block";
						id("ntube-winter-logo").style.visibility = "visible";
						if (window.location.pathname === "/") {
							id("ntube-winter-logo").style.width = "50px";
							id("ntube-winter-logo").style.height = "80px";
							id("ntube-winter-logo").style.left = "calc(50% - 80px)";
							id("ntube-winter-logo").style.top = "2px";
						}
					}
				} else if (isChristmas) {
					if (id("ntube-winter-logo") != null) {
						id("ntube-winter-logo").style.display = "block";
						id("ntube-winter-logo").style.visibility = "visible";
					}
				}
				const bodyCSS = id("body-css");
				if (bodyCSS !== null && bodyCSS !== undefined) {
					if (isSpooky) {
						bodyCSS.innerHTML = "body {background-color: #0d1126;color: #d98518;}";
						window.hasThemeEventActive = true;
					} else {
						/*const ui_theme = "ntube2_dark";
						if (ui_theme === "ntube2_dark") {
							// DO NOTHING, this is the default theme.
						}else if (ui_theme === "ntube2_light") {
							id("body-css").innerHTML = "body {background-color: white;color: black;}";
							const pyicss = id("poly-item-css");
							if (pyicss !== null && pyicss !== undefined) {
								pyicss.innerHTML = ".poly-item {padding-left: 5px; box-shadow: 0 14px 28px rgba(0, 0, 0, 0.08), 0 10px 10px rgba(0, 0, 0, 0.08); border: 1px solid rgba(0, 0, 0, 0.2); margin-bottom: 10px;}";
							}
						}*/
					}
				}
			}
		});
		libWeb.TaskDispatcher.run_task(function () {
			const set = nTubeSettings.auto();
			if (id("commander_overlay") !== null) {
				if (set["global"]["enable_commander"] && set["global"]["enable_commander"] === true) {
					UICommander.init();
				}
			}
		}, libWeb.TaskDispatcher.TaskPriority.BACKGROUND_TASK);
		libWeb.TaskDispatcher.run_task(function () {
			if (!hasSVGSupport()) {
				return;
			} else {
				const alts = document.querySelectorAll("[svg-src-alt]");
				if (alts !== null && alts !== undefined && alts.length > 0) {
					for (var jx = 0; jx < alts.length; jx++) {
						const altImg = alts[jx];
						altImg.src = altImg.getAttribute("svg-src-alt");
						if (altImg.hasAttribute("svg-width-alt")) {
							altImg.setAttribute("width", altImg.getAttribute("svg-width-alt"));
						}
						if (altImg.hasAttribute("svg-height-alt")) {
							altImg.setAttribute("height", altImg.getAttribute("svg-height-alt"));
						}
					}
				}
			}
		}, libWeb.TaskDispatcher.TaskPriority.BACKGROUND_TASK);
	},
	initTheming: function () {
		if (!window.nTubeSettings) {
			console.error("initTheming() -> nTubeSettings is missing!");
			return;
		}
		const set = nTubeSettings.auto();
		const use_lighter_theme = set["appearance"]["enable_new_look"];
		const bodyCSS = id("body-css");
		if (set["appearance"]["ui_use_narrow_font_spacing"]) {
			const of = document.createElement("style");
			of.innerHTML = "html, head, body { letter-spacing: -0.021em; }";
			document.body.appendChild(of);
		}
		if (bodyCSS !== null && bodyCSS !== undefined && !window.hasThemeEventActive) {
			libWeb.TaskDispatcher.run_promise(function () {
				const ui_theme = set["appearance"]["theme"];
				console.debug("ui_theme: " + ui_theme);
				if (ui_theme === "ntube2_dark") {
					// DO NOTHING, this is the default theme.
				} else if (ui_theme === "ntube2_light") {
					id("body-css").innerHTML = "body {background-color: white;color: black;}";
					const ntmml = id("ntube_main_menu_tube_label");
					console.debug("use_lighter_theme: " + use_lighter_theme);
					const oa = document.createElement("style");
					/* removed: padding-left: 5px; */
					if (use_lighter_theme) {
						oa.innerHTML =
							".message_div {background: rgba(255, 255, 255, 0.9);} #channel_description {color: white;} #search_overlay {background-color: rgba(255, 255, 255, 0.9);} .poly-item {box-shadow: 0 14px 28px rgba(0, 0, 0, 0.08), 0 10px 10px rgba(0, 0, 0, 0.08); border: 1px solid rgba(0, 0, 0, 0.2); margin-bottom: 10px;}";
					} else {
						oa.innerHTML =
							".message_div {background: rgba(255, 255, 255, 0.9);} #channel_description {color: white;} #search_overlay {background-color: rgba(255, 255, 255, 0.9);} .poly-item {box-shadow: 0 14px 28px rgba(0, 0, 0, 0.15), 0 10px 10px rgba(0, 0, 0, 0.12); border: 1px solid black; margin-bottom: 10px;}";
					}
					document.body.appendChild(o);
					if (ntmml !== null && ntmml !== undefined) {
						ntmml.style.color = "green";
					}
				} else if (ui_theme === "ntube2_casino") {
					id("body-css").innerHTML =
						"html, body, head { background: radial-gradient(#A27F43, #CBB484, #87745C, #998F76), url('/resources/noise.svg'); color: #E5E2DB; background-blend-mode: multiply; background-color: #A27F43; background-attachment: fixed; } .poly-item { background: initial; } .menu { background-color: rgba(0, 0, 0, 0.6); backdrop-filter: blur(5px); -webkit-backdrop-filter: blur(5px);}";
				} else if (ui_theme === "ntube2_casino_lightweight") {
					id("body-css").innerHTML =
						"html, body, head { background: radial-gradient(#A27F43, #CBB484, #87745C, #998F76); color: #E5E2DB; background-blend-mode: multiply; background-color: #A27F43; background-attachment: local; } .poly-item { background: initial; } .menu { background-color: rgba(0, 0, 0, 0.6); backdrop-filter: blur(5px); -webkit-backdrop-filter: blur(5px);}";
				} else if (ui_theme === "ntube2_rouge_casino") {
					id("body-css").innerHTML =
						"html, body, head { background: radial-gradient(#EB712C, #9F3E1D, #852811, #40170C), url('/resources/noise.svg'); color: #AFA29D; background-blend-mode: color; background-color: #40170C; background-attachment: fixed; } .poly-item { background: initial; } .menu { background-color: rgba(0, 0, 0, 0.6); backdrop-filter: blur(5px); -webkit-backdrop-filter: blur(5px);}";
				} else if (ui_theme === "ntube2_rouge_casino_lightweight") {
					id("body-css").innerHTML =
						"html, body, head { background: radial-gradient(#EB712C, #9F3E1D, #852811, #40170C); color: #AFA29D; background-blend-mode: color; background-color: #40170C; background-attachment: local; } .poly-item { background: initial; } .menu { background-color: rgba(0, 0, 0, 0.6); backdrop-filter: blur(5px); -webkit-backdrop-filter: blur(5px);}";
				} else if (ui_theme === "ntube2_pinky_light") {
					id("body-css").innerHTML =
						"html, body, head { background-color: #FFFCFD; color: #250E07 !important; } .poly-item { background: initial; border: 1px solid #C97270 !important; } .menu { background-color: rgba(0, 0, 0, 0.6); backdrop-filter: blur(5px); -webkit-backdrop-filter: blur(5px);} #search_button, #search_bar, #channel_info_div, #video-message {color: white !important;} .collapsible {border-bottom: 2px solid #C97270 !important;} .video-advanced-sep {border-top: 2px solid #C97270 !important;} #ntube-logo-you {color: #C97270 !important;} #search-top-ui, #_search_results {color: inherit !important;}";
					console.debug("use_lighter_theme: " + use_lighter_theme);
					const ob = document.createElement("style");
					/* removed: padding-left: 5px; */
					if (use_lighter_theme) {
						ob.innerHTML =
							".message_div {background: rgba(255, 255, 255, 0.9);} #channel_description {color: white;} #search_overlay {background-color: rgba(255, 255, 255, 0.9);} .poly-item {box-shadow: 0 14px 28px rgba(0, 0, 0, 0.08), 0 10px 10px rgba(0, 0, 0, 0.08); border: 1px solid rgba(0, 0, 0, 0.2); margin-bottom: 10px;}";
					} else {
						ob.innerHTML =
							".message_div {background: rgba(255, 255, 255, 0.9);} #channel_description {color: white;} #search_overlay {background-color: rgba(255, 255, 255, 0.9);} .poly-item {box-shadow: 0 14px 28px rgba(0, 0, 0, 0.15), 0 10px 10px rgba(0, 0, 0, 0.12); border: 1px solid black; margin-bottom: 10px;}";
					}
					document.body.appendChild(o);
					const ntmml1 = id("ntube_version_main_menu_label");
					if (ntmml1 !== null && ntmml1 !== undefined) {
						ntmml1.style.color = "white";
					}
				} else if (ui_theme === "ntube2_pinky") {
					id("body-css").innerHTML =
						"html, body, head { background-color: #E4C6C6; color: #1C1412 !important; } .poly-item { background: initial; border: 1px solid #C97270 !important; } .menu { background-color: rgba(0, 0, 0, 0.6); backdrop-filter: blur(5px); -webkit-backdrop-filter: blur(5px);} #search_button, #search_bar, #channel_info_div, #video-message {color: white !important;} .collapsible {border-bottom: 2px solid #C97270 !important;} .video-advanced-sep {border-top: 2px solid #C97270 !important;} #ntube-logo-you {color: #E44951 !important;} #search-top-ui, #_search_results {color: inherit !important;}";
					console.debug("use_lighter_theme: " + use_lighter_theme);
					const oc = document.createElement("style");
					/* removed: padding-left: 5px; */
					if (use_lighter_theme) {
						oc.innerHTML =
							".message_div {background: rgba(255, 255, 255, 0.9);} #channel_description {color: white;} #search_overlay {background-color: rgba(255, 255, 255, 0.9);} .poly-item {box-shadow: 0 14px 28px rgba(0, 0, 0, 0.08), 0 10px 10px rgba(0, 0, 0, 0.08); border: 1px solid rgba(0, 0, 0, 0.2); margin-bottom: 10px;}";
					} else {
						oc.innerHTML =
							".message_div {background: rgba(255, 255, 255, 0.9);} #channel_description {color: white;} #search_overlay {background-color: rgba(255, 255, 255, 0.9);} .poly-item {box-shadow: 0 14px 28px rgba(0, 0, 0, 0.15), 0 10px 10px rgba(0, 0, 0, 0.12); border: 1px solid black; margin-bottom: 10px;}";
					}
					document.body.appendChild(o);
					const ntmml2 = id("ntube_version_main_menu_label");
					if (ntmml2 !== null && ntmml2 !== undefined) {
						ntmml2.style.color = "white";
					}
				} else if (ui_theme === "ntube2_vivid_forest") {
					id("body-css").innerHTML =
						"html, body, head { background-color: #45425A; color: #ACC12F !important; } .poly-item { background: initial; border: 1px solid #575C55 !important; } .menu { background-color: rgba(0, 0, 0, 0.6); backdrop-filter: blur(5px); -webkit-backdrop-filter: blur(5px);}";
				} else if (ui_theme === "ntube2_neon_purple") {
					id("body-css").innerHTML =
						"html, body, head { background-color: #5D0936; color: #65b4ff !important; } .poly-item { background: initial; border: 1px solid #39346A !important; } .menu { background-color: rgba(0, 0, 0, 0.6); backdrop-filter: blur(5px); -webkit-backdrop-filter: blur(5px);} .collapsible {border-bottom: 2px solid #D7157E !important;} .video-advanced-sep {border-top: 2px solid #D7157E !important;} #ntube-logo-you {color: #D7157E !important;}";
				} else if (ui_theme === "ntube2_w95") {
					id("body-css").innerHTML =
						"#search_overlay, #channel_info_div, .message_div {color: silver;} html, body, head { background-color: #dfdfdf; color: #222 !important; } .poly-item {box-shadow: inset -1px -1px #0a0a0a, inset 1px 1px #fff, inset -2px -2px grey, inset 2px 2px #dfdfdf; background: initial; border: 1px solid silver !important; } .menu {color: silver !important; border-bottom: 2px solid !important; border-image: linear-gradient(to right, red 20%, yellow 20%, yellow 36%, green 36%, green 60%, blue 60%, blue 100%) 5;} ::-webkit-scrollbar {width: 16px;} ::-webkit-scrollbar-corner {background: #dfdfdf;} ::-webkit-scrollbar-thumb {background-color: #dfdfdf; box-shadow: inset -1px -1px #0a0a0a, inset 1px 1px #fff, inset -2px -2px grey, inset 2px 2px #dfdfdf;}";
				} else if (ui_theme === "ntube2_oscilloscope") {
					id("body-css").innerHTML =
						"html {background-color: black; color: green !important; font-family: monospace !important; font-size: 110%;} * {-webkit-font-smoothing: none !important; font-smooth: never !important;} .poly-item { border: 1px solid green !important; box-shadow: none; }";
				} else if (ui_theme === "ntube2_blood_red") {
					id("body-css").innerHTML = "html {background: linear-gradient(to top, #ff4800, 1%, black); color: red; }";
				}
			});
		}
	},
	confetti_deployment: function () {
		/* Proudly stolen from https://codepen.io/jonathanbell/pen/OvYVYw */
		var W = window.innerWidth;
		var H = window.innerHeight;
		const canvas = document.getElementById("--confetti-cannon");
		const context = canvas.getContext("2d");
		const maxConfettis = 128;
		const particles = [];

		const possibleColors = ["DodgerBlue", "OliveDrab", "Gold", "Pink", "SlateBlue", "LightBlue", "Gold", "Violet", "PaleGreen", "SteelBlue", "SandyBrown", "Chocolate", "Crimson"];

		function randomFromTo(from, to) {
			return Math.floor(Math.random() * (to - from + 1) + from);
		}

		function confettiParticle() {
			this.x = Math.random() * W; // x
			this.y = Math.random() * H - H; // y
			this.r = randomFromTo(11, 33); // radius
			this.d = Math.random() * maxConfettis + 11;
			this.color = possibleColors[Math.floor(Math.random() * possibleColors.length)];
			this.tilt = Math.floor(Math.random() * 33) - 11;
			this.tiltAngleIncremental = Math.random() * 0.07 + 0.05;
			this.tiltAngle = 0;

			this.draw = function () {
				context.beginPath();
				context.lineWidth = this.r / 2;
				context.strokeStyle = this.color;
				context.moveTo(this.x + this.tilt + this.r / 3, this.y);
				context.lineTo(this.x + this.tilt, this.y + this.tilt + this.r / 5);
				return context.stroke();
			};
		}

		function Draw() {
			const results = [];

			// Magical recursive functional love
			requestAnimationFrame(Draw);

			context.clearRect(0, 0, W, window.innerHeight);

			for (var i = 0; i < maxConfettis; i++) {
				results.push(particles[i].draw());
			}

			var particle = {};
			var remainingFlakes = 0;
			for (var i = 0; i < maxConfettis; i++) {
				particle = particles[i];

				particle.tiltAngle += particle.tiltAngleIncremental;
				particle.y += (Math.cos(particle.d) + 3 + particle.r / 2) / 2;
				particle.tilt = Math.sin(particle.tiltAngle - i / 3) * 15;

				if (particle.y <= H) remainingFlakes++;

				// If a confetti has fluttered out of view,
				// bring it back to above the viewport and let if re-fall.
				if (particle.x > W + 30 || particle.x < -30 || particle.y > H) {
					particle.x = Math.random() * W;
					particle.y = -30;
					particle.tilt = Math.floor(Math.random() * 10) - 20;
				}
			}

			return results;
		}

		window.addEventListener(
			"resize",
			function () {
				W = window.innerWidth;
				H = window.innerHeight;
				canvas.width = window.innerWidth;
				canvas.height = window.innerHeight;
			},
			false
		);

		// Push new confetti objects to `particles[]`
		for (var i = 0; i < maxConfettis; i++) {
			particles.push(new confettiParticle());
		}

		// Initialize
		canvas.width = W;
		canvas.height = H;
		Draw();
	},
};
window.nTube = nTube;
const nTubeUtils = nTube.nTubeUtils;

function fileSizeSI(size) {
	var e = (Math.log(size) / Math.log(1e3)) | 0;
	return +(size / Math.pow(1e3, e)).toFixed(2) + " " + ("kMGTPEZY"[e - 1] || "") + "B";
}

function getQueryVariable(variable) {
	var query = window.location.search.substring(1);
	var vars = query.split("&");
	for (var i = 0; i < vars.length; i++) {
		var pair = vars[i].split("=");
		if (decodeURIComponent(pair[0]) == variable) {
			return decodeURIComponent(pair[1]);
		}
	}
	return null;
}

function getQueryVariableString(string, variable) {
	var query = string.substring(string.indexOf("?") + 1);
	var vars = query.split("&");
	for (var i = 0; i < vars.length; i++) {
		var pair = vars[i].split("=");
		if (decodeURIComponent(pair[0]) == variable) {
			return decodeURIComponent(pair[1]);
		}
	}
	return null;
}

function hasQueryVariable(variable) {
	return getQueryVariable(variable) !== null;
}

function isHidden() {
	if (!document.hidden) return false;
	return document.hidden;
}

function isHidden(element) {
	if (element === null || element === undefined || element.style === null || element.style === undefined) return false;
	return element.style.display === "none" || element.style.visibility === "hidden";
}

function fadeout(e, callback) {
	var amounts = 32;
	var currentAmount = 1;
	var decrease = 1 / amounts;
	var n = setInterval(function () {
		if (!isHidden()) e.style.opacity = currentAmount -= decrease;
		if (currentAmount <= 0) {
			clearInterval(n);
			if (callback !== undefined && callback !== null) {
				callback();
			}
		}
	}, 25);
}
function fadein(e, callback) {
	var amounts = 32;
	var currentAmount = 0;
	var decrease = 1 / amounts;
	var n = setInterval(function () {
		if (!isHidden()) e.style.opacity = currentAmount += decrease;
		if (currentAmount >= 1) {
			clearInterval(n);
			if (callback !== undefined && callback !== null) {
				callback();
			}
		}
	}, 25);
}

function hide(element) {
	try {
		element.style.display = "none";
		element.style.visibility = "hidden";
	} catch (e) {
		console.error(e);
	}
}

function show(element) {
	try {
		element.style.display = "";
		element.style.visibility = "";
	} catch (e) {
		console.error(e);
	}
}

function waitForElementToDisplay(selector, callback, checkFrequencyInMs, timeoutInMs) {
	var startTimeInMs = Date.now();
	(function loopSearch() {
		if (document.querySelector(selector) != null) {
			callback();
			return;
		} else {
			setTimeout(function () {
				if (timeoutInMs && Date.now() - startTimeInMs > timeoutInMs) return;
				loopSearch();
			}, checkFrequencyInMs);
		}
	})();
}

function readSettings() {
	if (!canStoreData()) {
		return null;
	}
	try {
		if (localStorage.getItem("_ntube_settings") != null) {
			return localStorage.getItem("_ntube_settings");
		} else {
			return getCookie("settings");
		}
	} catch (e) {
		console.warn("readSettings() -> Warning: Are we running inside a sandboxed iframe?");
		console.error(e);
		return getCookie("settings");
	}
}

function readSettingsToJSON() {
	var rs = readSettings();
	if (rs == null) {
		return null;
	}
	return JSON.parse(base64Decode(rs));
}

function writeSettingsFromJSON(jsonObject) {
	var enc = base64Encode(JSON.stringify(jsonObject));
	setCookie("settings", enc, 9007199254740991);
	try {
		if (typeof Storage == "function") {
			localStorage.setItem("_ntube_settings", enc);
		}
	} catch (e) {
		console.error("writeSettingsFromJSON() -> Error: Are we running inside a sandboxed iframe?");
		console.error(e);
	}
}

function canPlayCodec(str) {
	if (window.MediaSource && "isTypeSupported" in window.MediaSource) {
		return MediaSource.isTypeSupported(str);
	} else if (window.ManagedMediaSource && "isTypeSupported" in window.ManagedMediaSource) {
		return ManagedMediaSource.isTypeSupported(str);
	} else if (window.HTMLMediaElement) {
		var __video = document.createElement("video");
		const x = __video.canPlayType(str);
		return x === "probably" || x === "maybe";
	} else {
		console.error("Unable to check if '" + str + "' is playable. [ERROR_MSE_MEDIA_MISSING]");
		return false;
	}
}

function _base64ToArrayBuffer(base64) {
	var binary_string = base64Decode(base64);
	var len = binary_string.length;
	var bytes = new Uint8Array(len);
	for (var i = 0; i < len; i++) {
		bytes[i] = binary_string.charCodeAt(i);
	}
	return bytes.buffer;
}

function seconds2time(seconds) {
	var hours = Math.floor(seconds / 3600);
	var minutes = Math.floor((seconds - hours * 3600) / 60);
	var seconds = seconds - hours * 3600 - minutes * 60;
	var time = "";
	if (hours != 0) {
		time = hours + ":";
	}
	if (minutes != 0 || time !== "") {
		minutes = minutes < 10 && time !== "" ? "0" + minutes : String(minutes);
		time += minutes + ":";
	}
	if (time === "") {
		time = seconds + "s";
	} else {
		time += seconds < 10 ? "0" + seconds : String(seconds);
	}
	return time;
}

function numberWithCommas(n) {
	var parts = n.toString().split(".");
	return parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ",") + (parts[1] ? "." + parts[1] : "");
}

function supportsVideoType(type) {
	var video = null;
	var formats = {
		ogg: 'video/ogg; codecs="theora"',
		h264: 'video/mp4; codecs="avc1.42E01E"',
		webm: 'video/webm; codecs="vp8, vorbis"',
		vp9: 'video/webm; codecs="vp9"',
		hls: 'application/x-mpegURL; codecs="avc1.42E01E"',
		av1: 'video/mp4; codecs="av01.0.04M.08"',
		av1_webm: 'video/webm; codecs="av01.0.04M.08"',
	};
	if (!video) {
		video = document.createElement("video");
	}
	if (window.MediaSource && window.MediaSource.isTypeSupported) {
		if (MediaSource.isTypeSupported(formats[type] || type)) return "probably";
		return "";
	} else if (window.ManagedMediaSource && window.ManagedMediaSource.isTypeSupported) {
		if (ManagedMediaSource.isTypeSupported(formats[type] || type)) return "probably";
		return "";
	} else {
		if (!video.canPlayType) {
			console.warn("video.canPlayType is missing! [fail at: " + type + "]");
			return "";
		}
		return video.canPlayType(formats[type] || type);
	}
}
window.supportsVideoType = supportsVideoType;

function supportsAV1playback() {
	if (typeof MediaSource != "function") {
		return supportsVideoType("av1") === "probably";
	}
	return MediaSource.isTypeSupported('video/mp4; codecs="av1"') || MediaSource.isTypeSupported('video/webm; codecs="av1"') || MediaSource.isTypeSupported('video/mp4; codecs="av01.0.04M.08"');
}

function isValidObject(obj) {
	return obj !== null && obj !== undefined;
}

function is_iPhoneOS() {
	var iDevices = ["iPad Simulator", "iPhone Simulator", "iPod Simulator", "iPad", "iPhone", "iPod"];

	if (!!navigator.platform) {
		while (iDevices.length) {
			if (navigator.platform === iDevices.pop()) {
				return true;
			}
		}
	}

	return false;
}

const __ie = (function () {
	if (window.ActiveXObject === undefined) return null; //Not IE
	if (!window.XMLHttpRequest) return 6;
	if (!document.querySelector) return 7;
	if (!document.addEventListener) return 8;
	if (!window.atob) return 9;
	if (!document.__proto__) return 10;
	return 11;
})();
window.ie = __ie;

function base64ToBlob(base64Data, contentType) {
	contentType = contentType || "";
	var sliceSize = 1024;
	var byteCharacters = base64Decode(base64Data);
	var bytesLength = byteCharacters.length;
	var slicesCount = Math.ceil(bytesLength / sliceSize);
	var byteArrays = new Array(slicesCount);

	for (var sliceIndex = 0; sliceIndex < slicesCount; ++sliceIndex) {
		var begin = sliceIndex * sliceSize;
		var end = Math.min(begin + sliceSize, bytesLength);

		var bytes = new Array(end - begin);
		for (var offset = begin, i = 0; offset < end; ++i, ++offset) {
			bytes[i] = byteCharacters[offset].charCodeAt(0);
		}
		byteArrays[sliceIndex] = new Uint8Array(bytes);
	}
	return new Blob(byteArrays, { type: contentType });
}

function clearArray(array) {
	while (array.length) {
		array.pop();
	}
}

function sendMessage(message) {
	sendMessage("nTube Notification", message, 5000);
}

function generateGUID() {
	if (window && window.crypto && window.crypto.randomUUID) {
		return window.crypto.randomUUID().toUpperCase();
	} else if (window.URL && window.URL.createObjectURL && window.URL.revokeObjectURL && window.Blob) {
		var temp_url = URL.createObjectURL(new Blob());
		var uuid = temp_url.toString();
		URL.revokeObjectURL(temp_url);
		return uuid.split(/[:\/]/g).pop();
	} else {
		var result, i, j;
		result = "";
		for (j = 0; j < 32; j++) {
			if (j == 8 || j == 12 || j == 16 || j == 20) result = result + "-";
			i = Math.floor(Math.random() * 16)
				.toString(16)
				.toUpperCase();
			result = result + i;
		}
		return result;
	}
}

function sendMessage() {
	var title = null;
	var message = null;
	var duration = null;
	var icon = null;
	const args = arguments;
	for (var i = 0; i < args.length; i++) {
		const arg = args[i];
		if (typeof arg === "string" && title === null) {
			title = arg;
		} else if (typeof arg === "string" && message === null) {
			message = arg;
		} else if (typeof arg === "number" && duration === null) {
			duration = arg;
		} else if (typeof arg === "string" && icon === null) {
			icon = arg;
		}
	}
	if (title === null) throw new Error("No title provided!");
	if (message === null) throw new Error("No message provided!");
	if (duration === null) throw new Error("No duration provided!");
	if (duration <= 0) throw new Error("Invalid duration provided!");
	if (typeof window.Notification === "function") {
		if (window.Notification.permission === "granted") {
			new Notification(title, {
				body: message,
				iconURL: icon,
			});
			return true;
		}
	}
	if (typeof window.displayMessage === "function") {
		window.displayMessage(message, duration);
		return true;
	}
	alert(message);
	return true;
}

if (!String.prototype.replaceAll) {
	String.prototype.replaceAll = function (searchStr, replaceStr) {
		var str = this;
		if (str.indexOf(searchStr) === -1) {
			return str;
		}
		return str.replace(searchStr, replaceStr).replaceAll(searchStr, replaceStr);
	};
}
if (!String.prototype.startsWith) {
	String.prototype.startsWith = function (searchString, position) {
		position = position || 0;
		return this.substr(position, searchString.length) === searchString;
	};
}
if (!String.prototype.contains) {
	String.prototype.contains = function (str) {
		return this.indexOf(str) > -1;
	};
}
Array.prototype.remByVal = function (val) {
	for (var i = 0; i < this.length; i++) {
		if (this[i] === val) {
			this.splice(i, 1);
			i--;
		}
	}
	return this;
};
Array.prototype.random = function () {
	if (window.performance && typeof window.performance.now === "function") {
		return this[0 | performance.now() % this.length];
	} else {
		return this[Math.floor(Math.random() * this.length)];
	}
};

const isMobile = navigator.userAgent.contains("Mobi") || navigator.userAgent.contains("Android") || navigator.userAgent.contains("iOS");
window.isMobile = isMobile;

// Use smaller buffers on devices claiming less than 2 GB of RAM.
const isLowMemoryDevice = "deviceMemory" in navigator && navigator.deviceMemory < 2;
if (isLowMemoryDevice) {
	console.warn("LOW_MEMORY_DEVICE flag is present, using smaller buffers.");
}
const ENABLE_DASH_QUIRKS = false;
function getBufferSize() {
	if (typeof InstallTrigger !== "undefined" && ENABLE_DASH_QUIRKS) {
		console.log("getBufferSize: QUIRK_FIX_DASH_BUFFERS is enabled!");
		return 30; // was 20
	}
	if (isMobile || is_iPhoneOS()) {
		return 40;
	} else if (navigator.userAgent.contains("CrKey")) {
		return 45;
	} else if (isLowMemoryDevice) {
		return 60;
	} else {
		return 120;
	}
}

function getHQBufferSize() {
	if (typeof InstallTrigger !== "undefined" && ENABLE_DASH_QUIRKS) {
		console.log("getHQBufferSize: QUIRK_FIX_DASH_BUFFERS is enabled!");
		return 20; // was 10
	}
	if (isMobile || is_iPhoneOS()) {
		return 30;
	} else if (navigator.userAgent.contains("CrKey")) {
		return 25;
	} else if (isLowMemoryDevice) {
		return 30;
	} else {
		return 60;
	}
}

function loadScript(url, callback) {
	if (url.endsWith(".css")) {
		var style = document.createElement("style");
		style.src = url;
		if (style.onreadystatechange) style.onreadystatechange = callback;
		style.onload = callback;
		document.head.appendChild(style);
	} else {
		var script = document.createElement("script");
		script.type = "text/javascript";
		script.src = url;
		script.onreadystatechange = callback;
		script.onload = callback;
		document.head.appendChild(script);
	}
}

function loadScripts(urls, callback) {
	var loadedCount = 0;
	function multiCallback() {
		loadedCount++;
		if (loadedCount >= urls.length) {
			callback.call(this, arguments);
		}
	}
	for (var i = 0; i < urls.length; i++) {
		const url = urls[i];
		loadScript(url, multiCallback);
	}
}

var mergeJSON = function (target, add) {
	function isObject(obj) {
		if (typeof obj == "object") {
			for (var key in obj) {
				if (obj.hasOwnProperty(key)) {
					return true; // search for first object prop
				}
			}
		}
		return false;
	}
	for (var key in add) {
		if (add.hasOwnProperty(key)) {
			if (target[key] && isObject(target[key]) && isObject(add[key])) {
				this.mergeJSON(target[key], add[key]);
			} else {
				target[key] = add[key];
			}
		}
	}
	return target;
};
window.mergeJSON = mergeJSON;

function containsAny(str, substrings) {
	if (substrings === null || substrings === undefined || substrings.length === 0) return null;
	for (var i = 0; i != substrings.length; i++) {
		var substring = substrings[i];
		if (str.indexOf(substring) != -1) {
			return substring;
		}
	}
	return null;
}

function forceRedraw(element) {
	const disp = element.style.display;
	element.style.display = "none";
	const trick = element.offsetHeight;
	element.style.display = disp;
}

function supportsWebGL2() {
	try {
		var canvas = document.createElement("canvas");
		var gl = canvas.getContext("webgl2");
		return gl && gl instanceof WebGL2RenderingContext;
	} catch (e) {
		console.error("supportsWebGL2() -> \n" + e);
		return false;
	}
}
function supportsWebGL() {
	try {
		var canvas = document.createElement("canvas");
		var gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl") || canvas.getContext("moz-webgl") || canvas.getContext("webkit-3d") || canvas.getContext("3d");
		return gl && gl instanceof WebGLRenderingContext;
	} catch (e) {
		console.error("supportsWebGL() -> \n" + e);
		return false;
	}
}

function supportsWebGPU() {
	try {
		return "gpu" in navigator;
	} catch (e) {
		console.error("supportsWebGPU() -> \n" + e);
		return false;
	}
}

function sleep(seconds) {
	const waitUntil = new Date().getTime() + seconds * 1000;
	while (new Date().getTime() < waitUntil) true;
}

function sleepMS(millis) {
	const waitUntil = new Date().getTime() + millis;
	while (new Date().getTime() < waitUntil) true;
}

/* NTUBE-MIGRATION-0002: Replace isES5 and isES6 with libWeb ESCompat. */

/*
 * A nobel letter to ECMAScript/JavaScript:
	FUCK YOU AND YOUR PROMISES

	I fucking hate languages which try to act smug and force asynchronous operations only,
	like come on, consider synchronous execution as fucking well, especially when you move
	between Web APIs.

	tl;dr even Java applets were fucking better than this crap
	(for people who agree, the ES6 variants are in base_es6_funcs.js)
 */

function isBattery() {
	if ((window.navigator && window.navigator.battery) || window.navigator.webkitBattery || window.navigator.mozBattery || window.navigator.msBattery) {
		var battery = navigator.battery || navigator.webkitBattery || navigator.mozBattery || navigator.msBattery;
		return !battery.charging;
	} else return false;
}

function copyToClipboard(text) {
	if (window.clipboardData && window.clipboardData.setData) {
		return window.clipboardData.setData("Text", text);
	} else if (document.queryCommandSupported && document.queryCommandSupported("copy")) {
		const textarea = document.createElement("textarea");
		textarea.textContent = text;
		textarea.style.opacity = "0";
		textarea.style.position = "fixed";
		document.body.appendChild(textarea);
		textarea.select();
		try {
			return document.execCommand("copy");
		} catch (ex) {
			console.warn("Copy to clipboard failed.", ex);
			return false;
		} finally {
			document.body.removeChild(textarea);
		}
	}
}

function isDataSaver() {
	if (window.navigator && window.navigator.connection) {
		if (navigator.connection.saveData) {
			return navigator.connection.saveData;
		}
	}
	return false;
}

function wantsReducedMotion() {
	if (window.matchMedia) {
		return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
	} else {
		return false;
	}
}

function isFocused(element) {
	if (element === null || element === undefined) {
		return false;
	}
	return document.activeElement === element;
}

const _URL_EXPRESSION = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/;

function isValidYTURL(str) {
	if (str === null || str === undefined || str.length === 0) {
		return false;
	} else {
		var _expr = new RegExp(_URL_EXPRESSION);
		if (!str.match(_expr)) {
			return false;
		} else {
			return (
				str.contains("youtu.be") ||
				str.contains("consent.youtube.com/m?continue=https%3A%2F%2Fwww.youtube.com%2Fwatch") ||
				str.contains("youtube.com/embed/") ||
				str.contains("youtube.com/live/") ||
				str.contains("youtube-nocookie.com/embed/") ||
				(str.contains("youtube.com") && str.contains("v=")) ||
				(str.contains("https://www.google.com/url?") && str.contains("&url=https%3A%2F%2Fwww.youtube.com"))
			);
		}
	}
}

function getVideoInfoFromURL(str) {
	if (!isValidYTURL(str)) {
		return {
			error: true,
			reason: "INVALID_URL",
			type: "invalid",
			url: str,
		};
	} else {
		if (str.contains("youtu.be") || str.contains("youtube.com/embed/") || str.contains("youtube-nocookie.com/embed/") || str.contains("youtube.com/live/")) {
			if (str.contains("watch?v")) {
				// We shouldn't be here.
				return getVideoInfoFromURL(str);
			} else if (str.contains("?enablejsapi=1")) {
				return {
					error: true,
					reason: "INVALID_URL",
					type: "invalid",
					url: str,
				};
			} else {
				var type;
				if (str.contains("youtube.com/embed/") || str.contains("youtube-nocookie.com/embed/")) {
					type = "embed";
				} else if (str.contains("youtube.com/live/")) {
					type = "live";
				} else {
					type = "short";
				}
				if (str.contains("?")) {
					return {
						id: str.substring(str.lastIndexOf("/") + 1, str.lastIndexOf("?")),
						playlist_id: getQueryVariableString(str, "playlist"),
						time: getQueryVariableString(str, "t"),
						type: type,
					};
				} else {
					return {
						id: str.substring(str.lastIndexOf("/") + 1),
						playlist_id: getQueryVariableString(str, "playlist"),
						time: getQueryVariableString(str, "t"),
						type: type,
					};
				}
			}
		} else if (str.contains("consent.youtube.com/m?continue=https%3A%2F%2Fwww.youtube.com%2Fwatch")) {
			var newURL = getQueryVariable(str, "continue");
			var videoID = getQueryVariableString(newURL, "v");
			var time = getQueryVariableString(newURL, "t");
			return {
				id: videoID,
				time: time,
				type: "standard-consent",
			};
		} else if (str.contains("youtube.com") && str.contains("v=")) {
			const _id = getQueryVariableString(str, "v");
			if (_id === null || _id === undefined) {
				return {
					error: true,
					reason: "UNKNOWN_URL_TYPE",
					url: str,
					type: "invalid",
				};
			} else {
				return {
					id: _id,
					playlist_id: getQueryVariableString(str, "playlist"),
					time: getQueryVariableString(str, "t"),
					type: "standard",
				};
			}
		} else if (str.contains("https://www.google.com/url?") && str.contains("&url=https%3A%2F%2Fwww.youtube.com")) {
			const _url = getQueryVariableString(str, "url");
			return getVideoInfoFromURL(_url);
		} else {
			return {
				error: true,
				reason: "UNKNOWN_URL_TYPE",
				url: str,
				type: "invalid",
			};
		}
	}
}

function round(value, decimals) {
	return Number(Math.round(value + "e" + decimals) + "e-" + decimals).toFixed(decimals);
}

function roundAndStrip(value, decimals) {
	return Number(value.toFixed(decimals));
}

function cloneJSON(obj) {
	if (obj === null || obj === undefined || typeof obj !== "object") {
		return obj;
	}
	if (obj instanceof Array) {
		var cloneA = [];
		for (var i = 0; i < obj.length; ++i) {
			cloneA[i] = cloneJSON(obj[i]);
		}
		return cloneA;
	}
	var cloneO = {};
	for (var i in obj) {
		cloneO[i] = cloneJSON(obj[i]);
	}
	return cloneO;
}

function hasServiceWorker() {
	return navigator && navigator.serviceWorker && "controller" in navigator.serviceWorker && navigator.serviceWorker.controller !== null && navigator.serviceWorker.controller !== undefined;
}

function supportsHLSWorkedAround(mainPlayer) {
	const __supports_hls = mainPlayer.canPlayType("application/vnd.apple.mpegurl") || mainPlayer.canPlayType("application/x-mpegURL");
	if (navigator.userAgent.contains("Android")) {
		/* I cannot be bothered to figure out why Android's native HLS hates me. */
		return false;
	}
	return __supports_hls;
}

function supportsPlayer(name, disablePlayers, isLiveStream, ENABLE_IOS_HLS) {
	/* Code boilerplate. */
	if (disablePlayers || isLiveStream || ENABLE_IOS_HLS) return false;
	/* IE-11 won't parse any player. */
	if (libWeb.BrowserEnvironment.isIE11) return false;
	switch (name) {
		case "nMuxer":
			return window.MediaSource || window.webkitMediaSource;
		case "rx-player":
		case "dash":
			return libWeb.ESCompat.checkIfCompatible(6) && typeof Object.setPrototypeOf === "function";
		default:
			return false;
	}
}

function isolateFrame(iframe) {
	iframe.credentialless = true;
	iframe.referrerPolicy = "same-origin";
	iframe.setAttribute("referrerpolicy", "same-origin");
	iframe.setAttribute(
		"sandbox",
		"allow-scripts ''; allow-same-origin ''; accelerometer 'none'; ambient-light-sensor 'none'; autoplay 'none'; battery 'none'; browsing-topics 'none'; camera 'none'; display-capture 'none'; domain-agent 'none'; document-domain 'none'; encrypted-media 'none'; execution-while-not-rendered ''; execution-while-out-of-viewport ''; gamepad 'none'; geolocation 'none'; gyroscope 'none'; hid 'none'; identity-credentials-get 'none'; idle-detection 'none'; local-fonts 'none'; magnetometer 'none'; microphone 'none'; midi 'none'; otp-credentials 'none'; payment 'none'; picture-in-picture 'none'; publickey-credentials-create 'none'; publickey-credentials-get 'none'; screen-wake-lock 'none'; serial 'none'; speaker-selection 'none'; usb 'none'; window-management 'none'; xr-spatial-tracking 'none'"
	);
	iframe.setAttribute("csp", "sandbox");
}

console.log("%cYou have opened your browser's Development Tools.\nOnly enter scripts here if you know what you are doing!", "color:#fff;background-color:#f00;font-size:28px");
console.debug("[nTube] Running on: " + libWeb.BrowserEnvironment.getBrowserVersion());
